package de.softwareforge.published.proxy;

/*
 * Copyright (C) 2007-2008 Henning P. Schmiedehausen
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import java.lang.reflect.Array;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;


/**
 * A factory that wraps the various Doc elements into proxies. This allows the Doclet to do its magic.
 *
 * @author <a href="mailto:henning@schmiedehausen.org">Henning P. Schmiedehausen</a>
 * @version $Id: ProxyFactory.java 102 2008-02-24 22:14:11Z hps@intermeta.de $
 * @api.level published unstable
 */

public class ProxyFactory
{
    private final Map<String, Invoker> methodMap = new HashMap<String, Invoker>();

    private final ProxyController proxyController;

    /**
     * Creates a new ProxyFactory object.
     *
     * @param proxyController The proxy controller which sets the policy for this factory.
     */
    public ProxyFactory(final ProxyController proxyController)
    {
        this.proxyController = proxyController;

        initInvokerMap();
    }

    /**
     * Wrap the given object in a proxy. This is controlled by the policy set by the
     * ProxyController. This is where the doclet specific code is sneaked in.
     *
     * @param obj The object to wrap.
     * @param wantedType The type that the proxy should implement.
     *
     * @return Either a proxy wrapped object or the object itself depending on the proxy controller policy.
     */
    public Object wrapProxy(final Object obj, final Class wantedType)
    {
        if (obj == null)
        {
            return null;
        }

        Class clazz = obj.getClass();

        // We unwrap arrays to check the objects inside. If they match the
        // criteria, wrap them into proxies, too.

        if (clazz.isArray())
        {
            Class componentType = wantedType.getComponentType();

            // Ooof. This is a method that returns e.g. 'Object' but an array object is returned.
            // fall back on the Array component type.
            if (componentType == null)
            {
                componentType = clazz.getComponentType();
            }

            Object[] array = (Object[]) obj;
            List<Object> list = new ArrayList<Object>(array.length);

            for (int i = 0; i < array.length; i++)
            {
                Object entry = array[i];

                // Do not pull this line into the veto below. Every element
                // in the array must be wrapped, regardless whether it will be
                // in the return array or not.
                entry = wrapProxy(entry, componentType);

                if (!proxyController.vetoAddToArray(entry, componentType))
                {
                    list.add(entry);
                }
            }

            return list.toArray((Object[]) Array.newInstance(componentType, list.size()));
        }
        else
        {
            if (proxyController.vetoWrapInProxy(obj))
            {
                return obj;
            }
            else
            {
                return Proxy.newProxyInstance(clazz.getClassLoader(), clazz.getInterfaces(), new UnwrapableInvocationHandler(obj));
            }
        }
    }

    /**
     * Initializes the invokers to be run from the InvocationHandler. Can not be static or the members of that map can not access
     * the members of the proxy factory.
     */
    private void initInvokerMap()
    {
        methodMap.put("compareTo", new UnwrapInvoker());
        methodMap.put("equals", new UnwrapInvoker());
        methodMap.put("overrides", new UnwrapInvoker());
        methodMap.put("subclassOf", new UnwrapInvoker());

        methodMap.putAll(proxyController.getInvokers());
    }

    private class UnwrapableInvocationHandler
        implements InvocationHandler
    {
        private final Object target;

        /**
         * Creates a new UnwrapableInvocationHandler object.
         *
         * @param target The object to wrap.
         */
        private UnwrapableInvocationHandler(final Object target)
        {
            this.target = target;
        }

        /**
         * @see InvocationHandler#invoke(Object, Method, Object[])
         */
        public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable
        {
            String methodName = method.getName();

            Invoker invoker = methodMap.get(methodName);

            try
            {
                Object result = null;

                if (invoker != null)
                {
                    result = invoker.invoke(target, method, args);
                }
                else
                {
                    result = method.invoke(target, args);
                }

                return wrapProxy(result, method.getReturnType());
            }
            catch (InvocationTargetException e)
            {
                throw e.getTargetException();
            }
        }

        private Object getTarget()
        {
            return target;
        }
    }

    private static class UnwrapInvoker
            implements Invoker
    {
        private UnwrapInvoker()
        {
        }

        /**
         * @see Invoker#invoke(Object, Method, Object[])
         */
        public Object invoke(final Object target, final Method method, final Object[] args) throws Throwable
        {
            assert(args != null && args.length == 1);

            args[0] = unwrap(args[0]);

            return method.invoke(target, args);
        }

        private Object unwrap(final Object proxy)
        {
            if (proxy instanceof Proxy)
            {
                return ((UnwrapableInvocationHandler) Proxy.getInvocationHandler(proxy)).getTarget();
            }

            return proxy;
        }
    }
}
